use super::r#type::{TypeCache, TYPE_CACHE};
use crate::model::{
    date::{Date, Day, Month, Week, Year},
    geoip::GeoIpItem,
};
use chrono::{DateTime, Datelike};
use maxminddb::{
    geoip2::{self},
    MaxMindDBError, Reader,
};
use network_scanner_server_model::RecordEntry;
use rocket_db_pools::sqlx::{self, Row, Sqlite, Transaction};
use std::{
    collections::{BTreeMap, HashMap},
    hash::{DefaultHasher, Hash, Hasher},
    str::FromStr,
    sync::Arc,
};

pub type GeoIpStats = HashMap<GeoIpItem, u32>;

#[derive(Clone)]
pub struct GeoIpIdItem<'r> {
    pub region: (u32, &'r str),
    pub province: Option<(u32, &'r str)>,
}

impl<'r> PartialEq for GeoIpIdItem<'r> {
    fn eq(&self, other: &Self) -> bool {
        if self.region.0 != other.region.0 {
            return false;
        }
        let id_self = self.province.as_ref().map(|item| item.0);
        let id_other = other.province.as_ref().map(|item| item.0);
        id_self == id_other
    }
}

impl<'r> Eq for GeoIpIdItem<'r> {}

impl<'r> Hash for GeoIpIdItem<'r> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.region.0.hash(state);
        if let Some((id, _)) = self.province {
            id.hash(state);
        }
    }
}

pub trait GeoIpItemExt<'r> {
    fn region(&self) -> &'r str;
    fn province(&self) -> Option<&'r str>;
}

impl<'r> From<GeoIpIdItem<'r>> for GeoIpItem {
    fn from(value: GeoIpIdItem) -> Self {
        GeoIpItem {
            region: String::from_str(value.region.1).unwrap(),
            province: value.province.map(|item| String::from_str(item.1).unwrap()),
        }
    }
}

impl<'r> GeoIpItemExt<'r> for GeoIpIdItem<'r> {
    fn region(&self) -> &'r str {
        self.region.1
    }

    fn province(&self) -> Option<&'r str> {
        self.province.as_ref().map(|item| item.1)
    }
}

impl<'r> GeoIpItemExt<'r> for &'r GeoIpItem {
    fn region(&self) -> &'r str {
        &self.region
    }

    fn province(&self) -> Option<&'r str> {
        self.province.as_deref()
    }
}

pub trait RecordEntryGeoIpExt {
    async fn get_geoip_item<'r>(
        &self,
        geoip_reader: &'r Reader<Vec<u8>>,
    ) -> Option<Vec<GeoIpIdItem<'r>>>;
}

impl RecordEntryGeoIpExt for RecordEntry {
    async fn get_geoip_item<'r>(
        &self,
        geoip_reader: &'r Reader<Vec<u8>>,
    ) -> Option<Vec<GeoIpIdItem<'r>>> {
        let type_id = self.type_id;
        let cache: Option<Arc<TypeCache>> = {
            let statement_read_lock = TYPE_CACHE.read().await;
            statement_read_lock
                .as_ref()
                .expect("Type cache is not initialized")
                .get(&type_id)
                .cloned()
        };
        let cache = match cache {
            Some(cache) => cache,
            None => return None,
        };

        let entry = &cache.r#type.entry;
        let mut results = Vec::new();
        for (index, record_entry) in entry.ip_addr.iter().enumerate() {
            if record_entry.geoip {
                let address = self.ip_addr[index];
                let info: geoip2::City = match geoip_reader.lookup(address) {
                    Ok(info) => info,
                    Err(err) => {
                        match err {
                            MaxMindDBError::AddressNotFoundError(_) => (),
                            other => eprintln!("Lookup GeoIP database failed: {:?}", other),
                        };
                        continue;
                    }
                };
                fn extract_map<'r>(names: Option<BTreeMap<&'r str, &'r str>>) -> Option<&'r str> {
                    let names = match names {
                        Some(names) => names,
                        None => return None,
                    };
                    match names.get("en") {
                        Some(name) => Some(*name),
                        None => None,
                    }
                }
                let country = match info.country {
                    Some(country) => {
                        let name = match extract_map(country.names) {
                            Some(name) => name,
                            None => continue,
                        };
                        let name_id = match country.geoname_id {
                            Some(geoname_id) => geoname_id,
                            None => {
                                let mut hasher = DefaultHasher::new();
                                name.hash(&mut hasher);
                                hasher.finish() as u32
                            }
                        };
                        (name_id, name)
                    }
                    None => continue,
                };
                let subdivison = info.subdivisions.and_then(|subdivisons| {
                    subdivisons.into_iter().next().and_then(|subdivison| {
                        let name = match extract_map(subdivison.names) {
                            Some(name) => name,
                            None => return None,
                        };
                        let name_id = match subdivison.geoname_id {
                            Some(geoname_id) => geoname_id,
                            None => {
                                let mut hasher = DefaultHasher::new();
                                name.hash(&mut hasher);
                                hasher.finish() as u32
                            }
                        };
                        Some((name_id, name))
                    })
                });
                results.push(GeoIpIdItem {
                    region: country,
                    province: subdivison,
                })
            }
        }
        Some(results)
    }
}

pub async fn update_geoip_time<'a, 'r, T: chrono::TimeZone, S: GeoIpItemExt<'r>>(
    db: &mut Transaction<'a, Sqlite>,
    time: DateTime<T>,
    increments: &HashMap<S, u32>,
) -> Result<(), sqlx::Error> {
    let year: u16 = time.year().try_into().unwrap();
    let month: u8 = time.month().try_into().unwrap();
    let day: u8 = time.day().try_into().unwrap();
    let week: u8 = time.iso_week().week().try_into().unwrap();
    update_geoip(db, Date::Year(year), increments).await?;
    update_geoip(db, Date::Month(Month { year, month }), increments).await?;
    update_geoip(db, Date::Week(Week { year, week }), increments).await?;
    update_geoip(db, Date::Day(Day { year, month, day }), increments).await?;
    Ok(())
}

pub async fn update_geoip<'a, 'r, S: GeoIpItemExt<'r>>(
    db: &mut Transaction<'a, Sqlite>,
    date: Date,
    increments: &HashMap<S, u32>,
) -> Result<(), sqlx::Error> {
    match date {
        Date::Year(year) => {
            for (item, increment) in increments {
                sqlx::query(
                    r#"
                        INSERT INTO geoip_yearly (year, region, province, count)
                        VALUES (?1, ?2, ?3, ?4)
                        ON CONFLICT(year, region, province)
                        DO UPDATE SET count = count + ?4
                    "#,
                )
                .bind(year)
                .bind(item.region())
                .bind(item.province())
                .bind(increment)
                .execute(&mut **db)
                .await?;
            }
        }
        Date::Month(month) => {
            for (item, increment) in increments {
                sqlx::query(
                    r#"
                    INSERT INTO geoip_monthly (id, year, month, region, province, count)
                    VALUES (?1, ?2, ?3, ?4, ?5, ?6)
                    ON CONFLICT(id)
                    DO UPDATE SET count = count + ?6
                "#,
                )
                .bind(month.get_id())
                .bind(month.year)
                .bind(month.month)
                .bind(item.region())
                .bind(item.province())
                .bind(increment)
                .execute(&mut **db)
                .await?;
            }
        }
        Date::Week(week) => {
            for (item, increment) in increments {
                sqlx::query(
                    r#"
                    INSERT INTO geoip_weekly (id, year, week, region, province, count)
                    VALUES (?1, ?2, ?3, ?4, ?5, ?6)
                    ON CONFLICT(id)
                    DO UPDATE SET count = count + ?6
                "#,
                )
                .bind(week.get_id())
                .bind(week.year)
                .bind(week.week)
                .bind(item.region())
                .bind(item.province())
                .bind(increment)
                .execute(&mut **db)
                .await?;
            }
        }
        Date::Day(day) => {
            for (item, increment) in increments {
                sqlx::query(
                    r#"
                    INSERT INTO geoip_daily (id, year, month, day, region, province, count)
                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
                    ON CONFLICT(id)
                    DO UPDATE SET count = count + ?7
                "#,
                )
                .bind(day.get_id())
                .bind(day.year)
                .bind(day.month)
                .bind(day.day)
                .bind(item.region())
                .bind(item.province())
                .bind(increment)
                .execute(&mut **db)
                .await?;
            }
        }
    };
    Ok(())
}

pub async fn get_year_geoip<'a>(
    db: &mut Transaction<'a, Sqlite>,
    year: Year,
) -> Result<GeoIpStats, sqlx::Error> {
    let items = sqlx::query(
        r#"
            SELECT region, province, count FROM geoip_yearly
            WHERE year = ?1
        "#,
    )
    .bind(year.0)
    .try_map(|row| {
        let region = row.try_get(0)?;
        let province = row.try_get(1)?;
        let stat = GeoIpItem { region, province };
        let count = row.try_get::<u32, usize>(2)?;
        Ok((stat, count))
    })
    .fetch_all(&mut **db)
    .await?;

    let mut result: GeoIpStats = HashMap::new();
    for (stat, count) in items {
        result.insert(stat, count);
    }
    Ok(result)
}

pub async fn get_month_geoip<'a>(
    db: &mut Transaction<'a, Sqlite>,
    month: Month,
) -> Result<GeoIpStats, sqlx::Error> {
    let items = sqlx::query(
        r#"
            SELECT region, province, count FROM geoip_monthly
            WHERE year = ?1 AND month = ?2
        "#,
    )
    .bind(month.year)
    .bind(month.month)
    .try_map(|row| {
        let region = row.try_get(0)?;
        let province = row.try_get(1)?;
        let stat = GeoIpItem { region, province };
        let count = row.try_get::<u32, usize>(2)?;
        Ok((stat, count))
    })
    .fetch_all(&mut **db)
    .await?;

    let mut result: GeoIpStats = HashMap::new();
    for (stat, count) in items {
        result.insert(stat, count);
    }
    Ok(result)
}

pub async fn get_week_geoip<'a>(
    db: &mut Transaction<'a, Sqlite>,
    week: Week,
) -> Result<GeoIpStats, sqlx::Error> {
    let items = sqlx::query(
        r#"
            SELECT region, province, count FROM geoip_weekly
            WHERE year = ?1 AND week = ?2
        "#,
    )
    .bind(week.year)
    .bind(week.week)
    .try_map(|row| {
        let region = row.try_get(0)?;
        let province = row.try_get(1)?;
        let stat = GeoIpItem { region, province };
        let count = row.try_get::<u32, usize>(2)?;
        Ok((stat, count))
    })
    .fetch_all(&mut **db)
    .await?;

    let mut result: GeoIpStats = HashMap::new();
    for (stat, count) in items {
        result.insert(stat, count);
    }
    Ok(result)
}

pub async fn get_daily_geoip<'a>(
    db: &mut Transaction<'a, Sqlite>,
    day: Day,
) -> Result<GeoIpStats, sqlx::Error> {
    let items = sqlx::query(
        r#"
            SELECT region, province, count FROM geoip_daily
            WHERE year = ?1 AND month = ?2 AND day = ?3
        "#,
    )
    .bind(day.year)
    .bind(day.month)
    .bind(day.day)
    .try_map(|row| {
        let region = row.try_get(0)?;
        let province = row.try_get(1)?;
        let stat = GeoIpItem { region, province };
        let count = row.try_get::<u32, usize>(2)?;
        Ok((stat, count))
    })
    .fetch_all(&mut **db)
    .await?;

    let mut result: GeoIpStats = HashMap::new();
    for (stat, count) in items {
        result.insert(stat, count);
    }
    Ok(result)
}
